iT邦幫忙

2025 iThome 鐵人賽

DAY 6
0
自我挑戰組

攜手 AI 從零開始打造一款 Flutter 應用程式系列 第 6

Day 6: 讓畫面動起來 - ListView 與 Card 的列表魔法

  • 分享至 

  • xImage
  •  

前言

大家好,歡迎來到第六天的旅程!在 Day 5,我們成功地從一張白紙打造出「省錢拍拍」的首頁,掌握了 ColumnRowContainer 的佈局技巧。

今天將學習 Flutter 中處理可滾動列表的王牌 Widget:ListView,並搭配 CardListTile,打造出一個既美觀又高效的動態交易紀錄列表。

Step 1: 為什麼 Column 不適用於長列表?

在 Flutter 中,ColumnRow 會試圖一次性地計算並渲染其所有的子元件。當子元件的總高度(對 Column 而言)或總寬度(對 Row 而言)超過螢幕的顯示範圍時,就會發生 Overflow 錯誤。

雖然我們可以使用 SingleChildScrollView 來包裹 Column 讓它變得可以滾動,但這是一個效能不佳的做法。因為它依然會一次性渲染列表中的所有項目,即使是那些在螢幕外根本看不到的項目。如果列表有數百個項目,這將會造成嚴重的性能問題。

Step 2: 高效能列表的答案 - ListView.builder

ListView.builder 是一個「懶加載 (lazy-loading)」的列表建構器。它的核心理念是:只創建和渲染那些當前正顯示在螢幕上的列表項。當使用者滾動列表時,它會智慧地回收滑出螢幕的項目,並創建即將進入螢幕的新項目。這種機制確保了即使有成千上萬筆資料,App 也能保持流暢的滾動性能。

ListView.builder 有兩個必填的核心屬性:

  • itemCount: 告訴 ListView 總共有多少個項目。
  • itemBuilder: 一個回呼函式 (Callback Function),它會接收 context 和項目索引 index,並根據這個索引回傳對應的列表項 Widget。

Step 3: 準備我們的假資料 (Mock Data)

在能夠連接後端資料庫之前,我們先在程式中創建一些假資料來模擬消費紀錄。

  1. lib/main.dart 檔案的頂部(import 語句下方),新增一個 Transaction 類別來定義我們資料的結構。
  2. HomePage 類別的內部,創建一個假資料列表。

請注意:由於 transactions 列表是在程式運行時才被建立的(它並非一個編譯時期的常數),我們必須移除 HomePage Widget 建構子前面的 const 關鍵字,Transaction 類別的建構子也是同理。

// lib/main.dart (在 import 下方)
class Transaction {
  final String title;
  final String category;
  final double amount;
  final DateTime date;

  // 我們也移除這個建構子的 const,因為 DateTime.now() 不是常數
  Transaction({
    required this.title,
    required this.category,
    required this.amount,
    required this.date,
  });
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      // 關閉右上角的 Debug 標籤
      debugShowCheckedModeBanner: false,
      title: 'SnapSaver',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      // 移除 'const' 關鍵字
      home: HomePage(),
    );
  }
}

class HomePage extends StatelessWidget {
  // 關鍵修正:移除 'const' 關鍵字
  HomePage({super.key});

  // 建立一個假資料列表
  final List<Transaction> transactions = [
    Transaction(title: '7-Eleven 便利商店', category: '餐飲', amount: 85, date: DateTime.now()),
    Transaction(title: '全聯福利中心', category: '購物', amount: 750, date: DateTime.now()),
    Transaction(title: '週末電影票', category: '娛樂', amount: 540, date: DateTime.now()),
    Transaction(title: '捷運月票', category: '交通', amount: 1280, date: DateTime.now()),
    Transaction(title: '電信費', category: '生活繳費', amount: 499, date: DateTime.now()),
    Transaction(title: '午餐:牛肉麵', category: '餐飲', amount: 180, date: DateTime.now()),
    Transaction(title: '蝦皮購物', category: '購物', amount: 1250, date: DateTime.now()),
  ];

  @override
  Widget build(BuildContext context) {
    // ...
  }
}

Step 4: 實作動態列表

現在,萬事俱備。我們將 Column 佈局與 ListView 結合起來。

重要觀念:當你在一個 Column 內部直接放置 ListView 時,會發生錯誤。因為 Column 給予子元件無限的垂直空間,但 ListView 需要一個有限的、確切的高度。為了解決這個問題,我們需要用 Expanded Widget 來包裹 ListView,告訴它:「請填滿 Column 中所有剩餘的空間」。

  • Expanded 是一個 **彈性佈局 (Flexible Layout)**的 Widget,用來在 RowColumn 中,
    讓子元件自動填滿剩餘的可用空間。

  • Card 是 Flutter 提供的一種 UI 容器,
    它模仿了 Material Design 的卡片風格,有圓角、陰影,在視覺上提供層次感與區隔。

修改 HomePage 的 build 方法:

// 分隔線
  const Padding(
     padding: EdgeInsets.symmetric(horizontal: 16.0),
     child: Divider(),
  ),
  // 使用 Expanded 包裹 ListView.builder
  Expanded(
    child: ListView.builder(
      itemCount: transactions.length,
      itemBuilder: (BuildContext context, int index) {
        final transaction = transactions[index];
        return Card(
          margin: const EdgeInsets.symmetric(
            horizontal: 16.0,
            vertical: 6.0,
          ),
          child: ListTile(
            leading: const Icon(
              Icons.receipt_long,
              color: Colors.deepPurple,
            ),
            title: Text(transaction.title),
            subtitle: Text(transaction.category),
            trailing: Text(
              'NT\$ ${transaction.amount.toStringAsFixed(0)}',
              style: const TextStyle(
                color: Colors.redAccent,
                fontWeight: FontWeight.bold,
              ),
            ),
          ),
        );
      },
    ),
  ),

ListView

程式碼解析:

  1. Column 的最下方,我們使用 Expanded 來包裹 ListView.builder,解決了高度限制的問題。
  2. itemCount 直接設為我們假資料列表的長度。
  3. itemBuilder 中,我們為每個列表項返回一個 Card Widget,內部包裹 ListTile 來呈現資料。
  4. Card 內部包裹著 ListTile,它極大地簡化了列表項的佈局。我們將 title, category, amount 等資料輕鬆地放入 ListTiletitle, subtitle, trailing 屬性中。

今日結語

今天我們學習了 Flutter 中,處理長列表最高效的元件之一:ListView.builder。不僅理解了它「懶加載」的核心原理,還學會了使用 Expanded 來處理它在 Column 中的佈局問題,並搭配 CardListTile 打造出專業、美觀的列表項。

明天,我們將來處理 App 的「門面」問題,深入探索 ThemeData,為「省錢拍拍」客製化一套專屬的色彩與字體主題。


上一篇
Day 5: UI 佈局基礎 - Row, Column, Container
下一篇
Day 7: App 的整體造型師 - 使用 ThemeData 打造專屬風格
系列文
攜手 AI 從零開始打造一款 Flutter 應用程式8
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言